Scopri come testare efficacemente le tue applicazioni FastAPI utilizzando TestClient. Approfondisci le migliori pratiche e i casi d'uso reali per API robuste e affidabili.
Padronanza dei Test FastAPI: Una Guida Completa a TestClient
FastAPI è emerso come un framework leader per la creazione di API ad alte prestazioni con Python. La sua velocità, facilità d'uso e convalida automatica dei dati lo rendono uno dei preferiti dagli sviluppatori di tutto il mondo. Tuttavia, un'API ben costruita è valida quanto i suoi test. Test approfonditi assicurano che la tua API funzioni come previsto, rimanga stabile sotto pressione e possa essere implementata con sicurezza in produzione. Questa guida completa si concentra sull'utilizzo di TestClient di FastAPI per testare efficacemente gli endpoint della tua API.
Perché i Test sono Importanti per le Applicazioni FastAPI?
I test sono un passaggio cruciale nel ciclo di vita dello sviluppo del software. Ti aiuta a:
- Identificare i bug in anticipo: Rilevare gli errori prima che raggiungano la produzione, risparmiando tempo e risorse.
- Garantire la qualità del codice: Promuovere codice ben strutturato e manutenibile.
- Prevenire le regressioni: Garantire che le nuove modifiche non interrompano le funzionalità esistenti.
- Migliorare l'affidabilità dell'API: Costruire fiducia nella stabilità e nelle prestazioni dell'API.
- Facilitare la collaborazione: Fornire una chiara documentazione del comportamento previsto per altri sviluppatori.
Introduzione a TestClient di FastAPI
FastAPI fornisce un TestClient integrato che semplifica il processo di test degli endpoint della tua API. TestClient agisce come un client leggero che può inviare richieste alla tua API senza avviare un server completo. Questo rende i test significativamente più veloci e convenienti.
Caratteristiche principali di TestClient:
- Simula le richieste HTTP: Ti consente di inviare richieste GET, POST, PUT, DELETE e altre richieste HTTP alla tua API.
- Gestisce la serializzazione dei dati: Serializza automaticamente i dati delle richieste (ad esempio, i payload JSON) e deserializza i dati delle risposte.
- Fornisce metodi di asserzione: Offre metodi convenienti per verificare il codice di stato, le intestazioni e il contenuto delle risposte.
- Supporta i test asincroni: Funziona perfettamente con la natura asincrona di FastAPI.
- Si integra con i framework di test: Si integra facilmente con i framework di test Python più diffusi come pytest e unittest.
Configurazione del tuo Ambiente di Test
Prima di iniziare a testare, devi configurare il tuo ambiente di test. Questo di solito comporta l'installazione delle dipendenze necessarie e la configurazione del tuo framework di test.
Installazione
Innanzitutto, assicurati di avere FastAPI e pytest installati. Puoi installarli usando pip:
pip install fastapi pytest httpx
httpx è un client HTTP che FastAPI utilizza sotto il cofano. Mentre TestClient fa parte di FastAPI, avere httpx installato garantisce un test senza problemi. Alcuni tutorial menzionano anche requests, tuttavia, httpx è più in linea con la natura asincrona di FastAPI.
Esempio di Applicazione FastAPI
Creiamo una semplice applicazione FastAPI che possiamo usare per i test:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Salva questo codice come main.py. Questa applicazione definisce tre endpoint:
/: Un semplice endpoint GET che restituisce un messaggio "Hello World"./items/{item_id}: Un endpoint GET che restituisce un elemento in base al suo ID./items/: Un endpoint POST che crea un nuovo elemento.
Scrivere il tuo Primo Test
Ora che hai un'applicazione FastAPI, puoi iniziare a scrivere test usando TestClient. Crea un nuovo file chiamato test_main.py nella stessa directory di main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
In questo test:
- Importiamo
TestCliente l'istanzaappdi FastAPI. - Creiamo un'istanza di
TestClient, passando l'app. - Definiamo una funzione di test
test_read_root. - All'interno della funzione di test, usiamo
client.get("/")per inviare una richiesta GET all'endpoint root. - Verifichiamo che il codice di stato della risposta sia 200 (OK).
- Verifichiamo che il JSON della risposta sia uguale a
{"message": "Hello World"}.
Esecuzione dei tuoi Test con pytest
Per eseguire i tuoi test, apri semplicemente un terminale nella directory contenente il tuo file test_main.py ed esegui il seguente comando:
pytest
pytest scoprirà ed eseguirà automaticamente tutti i test nel tuo progetto. Dovresti vedere un output simile a questo:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Test di Diversi Metodi HTTP
TestClient supporta tutti i metodi HTTP standard, inclusi GET, POST, PUT, DELETE e PATCH. Vediamo come testare ciascuno di questi metodi.
Test di Richieste GET
Abbiamo già visto un esempio di test di una richiesta GET nella sezione precedente. Ecco un altro esempio, che testa l'endpoint /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Questo test invia una richiesta GET a /items/1 con un parametro di query q=test. Quindi verifica che il codice di stato della risposta sia 200 e che il JSON della risposta contenga i dati previsti.
Test di Richieste POST
Per testare una richiesta POST, è necessario inviare i dati nel corpo della richiesta. TestClient serializza automaticamente i dati in JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
In questo test:
- Creiamo un dizionario
item_datacontenente i dati per il nuovo elemento. - Usiamo
client.post("/items/", json=item_data)per inviare una richiesta POST all'endpoint/items/, passandoitem_datacome payload JSON. - Verifichiamo che il codice di stato della risposta sia 200 e che il JSON della risposta corrisponda a
item_data.
Test di Richieste PUT, DELETE e PATCH
Testare le richieste PUT, DELETE e PATCH è simile al test delle richieste POST. È sufficiente utilizzare i metodi corrispondenti su TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Aggiungi asserzioni per la risposta prevista
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Aggiungi asserzioni per la risposta prevista
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Aggiungi asserzioni per la risposta prevista
Ricorda di aggiungere asserzioni per verificare che le risposte siano come previsto.
Tecniche di Test Avanzate
TestClient offre diverse funzionalità avanzate che possono aiutarti a scrivere test più completi ed efficaci.
Test con Dipendenze
Il sistema di iniezione delle dipendenze di FastAPI ti consente di iniettare facilmente le dipendenze negli endpoint della tua API. Durante i test, potresti voler sovrascrivere queste dipendenze per fornire implementazioni mock o specifiche per i test.
Ad esempio, supponi che la tua applicazione dipenda da una connessione al database. Puoi sovrascrivere la dipendenza del database nei tuoi test per utilizzare un database in-memory:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Configurazione del Database
DATABASE_URL = "sqlite:///./test.db" # Database in-memory per i test
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Definisci il Modello Utente
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# App FastAPI
app = FastAPI()
# Dipendenza per ottenere la sessione del database
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint per creare un utente
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Sovrascrivi la dipendenza del database per i test
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# Prima, assicurati che le tabelle vengano create, cosa che potrebbe non accadere per impostazione predefinita
Base.metadata.create_all(bind=engine) # importante: crea le tabelle nel database di test
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Pulisci la sovrascrittura dopo il test, se necessario
app.dependency_overrides = {}
Questo esempio sovrascrive la dipendenza get_db con una funzione specifica per il test che restituisce una sessione a un database SQLite in-memory. Importante: la creazione dei metadati deve essere esplicitamente invocata affinché il database di test funzioni correttamente. Non riuscire a creare la tabella porterà a errori relativi alle tabelle mancanti.
Test del Codice Asincrono
FastAPI è costruito per essere asincrono, quindi dovrai spesso testare il codice asincrono. TestClient supporta i test asincroni in modo trasparente.
Per testare un endpoint asincrono, definisci semplicemente la tua funzione di test come async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simula qualche operazione asincrona
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Necessario per essere compatibile con pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Nota: È necessario installare pytest-asyncio per utilizzare @pytest.mark.asyncio: pip install pytest-asyncio. È inoltre necessario assicurarsi che asyncio.get_event_loop() sia configurato se si utilizzano versioni precedenti di pytest. Se si utilizza pytest versione 8 o successiva, ciò potrebbe non essere necessario.
Test del Caricamento File
FastAPI semplifica la gestione dei caricamenti di file. Per testare i caricamenti di file, è possibile utilizzare il parametro files dei metodi di richiesta di TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
In questo test, creiamo un file fittizio usando io.BytesIO e lo passiamo al parametro files. Il parametro files accetta un elenco di tuple, dove ogni tupla contiene il nome del campo, il nome del file e il contenuto del file. Il tipo di contenuto è importante per una gestione accurata da parte del server.
Test della Gestione degli Errori
È importante testare come l'API gestisce gli errori. Puoi usare TestClient per inviare richieste non valide e verificare che l'API restituisca le risposte di errore corrette.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Questo test invia una richiesta GET a /items/101, che genera un'HTTPException con un codice di stato di 400. Il test verifica che il codice di stato della risposta sia 400 e che il JSON della risposta contenga il messaggio di errore previsto.
Test delle Funzionalità di Sicurezza
Se la tua API utilizza l'autenticazione o l'autorizzazione, dovrai testare anche queste funzionalità di sicurezza. TestClient ti consente di impostare intestazioni e cookie per simulare richieste autenticate.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Sicurezza
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simula l'autenticazione
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# Innanzitutto, ottieni un token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Quindi, usa il token per accedere alla route protetta
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # formato corretto.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
In questo esempio, testiamo l'endpoint di accesso e quindi usiamo il token ottenuto per accedere a una route protetta. Il parametro headers dei metodi di richiesta di TestClient ti consente di impostare intestazioni personalizzate, inclusa l'intestazione Authorization per i token bearer.
Best Practice per i Test FastAPI
Ecco alcune best practice da seguire quando si testano le applicazioni FastAPI:
- Scrivi test completi: Punta a un'elevata copertura dei test per garantire che tutte le parti della tua API siano testate a fondo.
- Usa nomi dei test descrittivi: Assicurati che i nomi dei tuoi test indichino chiaramente cosa sta verificando il test.
- Segui il modello Arrange-Act-Assert: Organizza i tuoi test in tre fasi distinte: Arrange (imposta i dati di test), Act (esegui l'azione da testare) e Assert (verifica i risultati).
- Usa oggetti mock: Mock le dipendenze esterne per isolare i tuoi test ed evitare di fare affidamento su sistemi esterni.
- Test di casi limite: Testa la tua API con input non validi o imprevisti per assicurarti che gestisca gli errori in modo corretto.
- Esegui i test frequentemente: Integra i test nel tuo flusso di lavoro di sviluppo per rilevare i bug in anticipo e spesso.
- Integra con CI/CD: Automatizza i tuoi test nella tua pipeline CI/CD per garantire che tutte le modifiche al codice siano testate a fondo prima di essere implementate in produzione. Strumenti come Jenkins, GitLab CI, GitHub Actions o CircleCI possono essere utilizzati per raggiungere questo obiettivo.
Esempio: Test di Internazionalizzazione (i18n)
Quando si sviluppano API per un pubblico globale, l'internazionalizzazione (i18n) è essenziale. Testare i18n implica verificare che la tua API supporti più lingue e regioni correttamente. Ecco un esempio di come puoi testare i18n in un'applicazione FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Questo esempio imposta l'intestazione Accept-Language per specificare la lingua desiderata. L'API restituisce il saluto nella lingua specificata. I test assicurano che l'API gestisca correttamente le diverse preferenze linguistiche. Se l'intestazione Accept-Language è assente, viene utilizzata la lingua predefinita "en".
Conclusione
I test sono una parte essenziale della creazione di applicazioni FastAPI robuste e affidabili. TestClient fornisce un modo semplice e conveniente per testare gli endpoint della tua API. Seguendo le best practice illustrate in questa guida, puoi scrivere test completi che garantiscono la qualità e la stabilità delle tue API. Dalle richieste di base alle tecniche avanzate come l'iniezione delle dipendenze e i test asincroni, TestClient ti consente di creare codice ben testato e manutenibile. Abbraccia i test come parte fondamentale del tuo flusso di lavoro di sviluppo e costruirai API potenti e affidabili per gli utenti di tutto il mondo. Ricorda l'importanza dell'integrazione CI/CD per automatizzare i test e garantire un controllo di qualità continuo.